#!/usr/bin/env python3
#
#    build-command-template: converts new style command definitions in XML
#      to the old style (bunch of dirs and node.def's) command templates
#
#    Copyright (C) 2017 VyOS maintainers <maintainers@vyos.net>
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
#    USA

import sys
import os
import argparse
import copy
import functools

from lxml import etree as ET
from textwrap import fill

# Defaults

#validator_dir = "/usr/libexec/vyos/validators"
validator_dir = "${vyos_validators_dir}"
default_constraint_err_msg = "Invalid value"


## Get arguments

parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates')
parser.add_argument('--debug', help='Enable debug information output', action='store_true')
parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file")
parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file")
parser.add_argument('OUTPUT_DIR', type=str, help="Output directory")

args = parser.parse_args()

input_file = args.INPUT_FILE
schema_file = args.SCHEMA_FILE
output_dir = args.OUTPUT_DIR
debug = args.debug

#debug = True

## Load and validate the inputs

try:
    xml = ET.parse(input_file)
except Exception as e:
    print("Failed to load interface definition file {0}".format(input_file))
    print(e)
    sys.exit(1)

try:
    relaxng_xml = ET.parse(schema_file)
    validator = ET.RelaxNG(relaxng_xml)

    if not validator.validate(xml):
        print(validator.error_log)
        print("Interface definition file {0} does not match the schema!".format(input_file))
        sys.exit(1)
except Exception as e:
    print("Failed to load the XML schema {0}".format(schema_file))
    print(e)
    sys.exit(1)

if not os.access(output_dir, os.W_OK):
    print("The output directory {0} is not writeable".format(output_dir))
    sys.exit(1)

## If we got this far, everything must be ok and we can convert the file

def make_path(l):
    path = functools.reduce(os.path.join, l)
    if debug:
        print(path)
    return path

def collect_validators(ve):
    regexes = []
    regex_elements = ve.findall("regex")
    if regex_elements is not None:
        regexes = list(map(lambda e: e.text.strip().replace('\\','\\\\'), regex_elements))
    if "" in regexes:
        print("Warning: empty regex, node will be accepting any value")

    validator_elements = ve.findall("validator")
    validators = []
    if validator_elements is not None:
        for v in validator_elements:
            v_name = os.path.join(validator_dir, v.get("name"))

            # XXX: lxml returns None for empty arguments
            v_argument = None
            try:
                v_argument = v.get("argument")
            except:
                pass
            if v_argument is None:
                v_argument = ""

            validators.append("{0} {1}".format(v_name, v_argument))


    regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes))
    validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators))

    return regex_args + " " + validator_args

def get_properties(p, default=None):
    props = {}

    if p is None:
        return props

    # Get the help string
    try:
        help = p.find("help").text
        if default != None:
            # DNS forwarding for instance has multiple defaults - specified as whitespace separated list
            tmp = ', '.join(default.text.split())
            help += f' (default: {tmp})'
        help = fill(help, width=64, subsequent_indent='\t\t\t')
        props["help"] = help
    except:
        pass

    # Get value help strings
    try:
        vhe = p.findall("valueHelp")
        vh = []
        for v in vhe:
            format = v.find("format").text
            description = v.find("description").text
            if default != None and default.text == format:
                description += f' (default)'
            # Is no description was specified, keep it empty
            if not description: description = ''
            vh.append( (format, description) )
        props["val_help"] = vh
    except:
        props["val_help"] = []

    # Get the constraint and constraintGroup statements

    error_msg = default_constraint_err_msg
    # Get the error message if it's there
    try:
        error_msg = p.find("constraintErrorMessage").text
    except:
        pass

    vce = p.find("constraint")

    distinct_validator_string = ""
    if vce is not None:
        # The old backend doesn't support multiple validators in OR mode
        # so we emulate it

        distinct_validator_string = collect_validators(vce)

    vcge = p.findall("constraintGroup")

    group_validator_string = ""
    if len(vcge):
        for vcg in vcge:
            group_validator_string = group_validator_string + " --grp " + collect_validators(vcg)

    if vce is not None or len(vcge):
        validator_script = '${vyos_libexec_dir}/validate-value'
        validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, distinct_validator_string, group_validator_string, error_msg)

        props["constraint"] = validator_string

    # Get the completion help strings
    try:
        che = p.findall("completionHelp")
        ch = ""
        for c in che:
            scripts = c.findall("script")
            paths = c.findall("path")
            lists = c.findall("list")

            # Current backend doesn't support multiple allowed: tags
            # so we get to emulate it
            comp_exprs = []
            for i in lists:
                comp_exprs.append(f'echo "{i.text}"')
            for i in paths:
                comp_exprs.append(f'/bin/cli-shell-api listNodes {i.text}')
            for i in scripts:
                comp_exprs.append(f'sh -c "{i.text}"')
            comp_help = ' && echo " " && '.join(comp_exprs)
            props["comp_help"] = comp_help
    except:
        props["comp_help"] = []

    # Get priority
    try:
        props["priority"] = p.find("priority").text
    except:
        pass

    # Get "multi"
    if p.find("multi") is not None:
        props["multi"] = True

    # Get "valueless"
    if p.find("valueless") is not None:
        props["valueless"] = True

    return props

def make_node_def(props):
    # XXX: replace with a template processor if it grows
    #      out of control

    node_def = ""

    if "tag" in props:
        node_def += "tag:\n"

    if "multi" in props:
        node_def += "multi:\n"

    if "type" in props:
        # Will always be txt in practice if it's set
        node_def += "type: {0}\n".format(props["type"])

    if "priority" in props:
        node_def += "priority: {0}\n".format(props["priority"])

    if "help" in props:
        node_def += "help: {0}\n".format(props["help"])

    if "val_help" in props:
        for v in props["val_help"]:
            node_def += "val_help: {0}; {1}\n".format(v[0], v[1])

    if "comp_help" in props:
        node_def += "allowed: {0}\n".format(props["comp_help"])

    if "constraint" in props:
        node_def += "syntax:expression: {0}\n".format(props["constraint"])

    shim = '${vyshim}'

    if "owner" in props:
        if "tag" in props:
            node_def += "end: sudo sh -c \"{1} VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"], shim)
        else:
            node_def += "end: sudo sh -c \"{1} {0}\"\n".format(props["owner"], shim)

    if debug:
        print("The contents of the node.def file:\n", node_def)

    return node_def

def process_node(n, tmpl_dir):
    # Avoid mangling the path from the outer call
    my_tmpl_dir = copy.copy(tmpl_dir)

    props_elem = n.find("properties")
    children = n.find("children")

    name = n.get("name")
    owner = n.get("owner")
    node_type = n.tag

    my_tmpl_dir.append(name)

    if debug:
        print("Name of the node: {0}. Created directory: {1}\n".format(name, "/".join(my_tmpl_dir)), end="")
    os.makedirs(make_path(my_tmpl_dir), exist_ok=True)

    props = get_properties(props_elem, n.find("defaultValue"))
    if owner:
        props["owner"] = owner
        # <priority> tag is mandatory if the parent node has an owner
        if "priority" not in props:
            raise ValueError(
                f"<priority> tag should be set for the node <{name}> path '{' '.join(my_tmpl_dir[1:])}'"
            )

    # Type should not be set for non-tag, non-leaf nodes
    # For non-valueless leaf nodes, set the type to txt: to make them have some type,
    # actual value validation is handled by constraints translated to syntax:expression:
    if node_type != "node":
        if "valueless" not in props.keys():
            props["type"] = "txt"
    if node_type == "tagNode":
        props["tag"] = "True"

    if node_type != "leafNode":
        if "multi" in props:
            raise ValueError("<multi/> tag is only allowed in <leafNode>")
        if "valueless" in props:
            raise ValueError("<valueless/> is only allowed in <leafNode>")

    nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")

    # Only create the "node.def" file if it exists but is empty, or if it does
    # not exist at all. An empty node.def file could be generated by XML paths
    # that derive from one another bot having a common base structure like
    # "protocols static"
    if not os.path.exists(nodedef_path) or os.path.getsize(nodedef_path) == 0:
        with open(nodedef_path, "w") as f:
            f.write(make_node_def(props))

    if node_type == "node":
        inner_nodes = children.iterfind("*")
        for inner_n in inner_nodes:
            process_node(inner_n, my_tmpl_dir)
    if node_type == "tagNode":
        my_tmpl_dir.append("node.tag")
        if debug:
            print("Created path for the tagNode:", end="")
        os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
        inner_nodes = children.iterfind("*")
        for inner_n in inner_nodes:
            process_node(inner_n, my_tmpl_dir)
    else:
        # This is a leaf node
        pass


root = xml.getroot()

nodes = root.iterfind("*")
for n in nodes:
    if n.tag == "syntaxVersion":
        continue
    try:
        process_node(n, [output_dir])
    except ValueError as e:
        print(e)
        sys.exit(1)
